contents

1. 제네릭이란?


2. 기본 문법과 사용 예시

제네릭 클래스 예시

class Box<T> {
    private T value;
    public void set(T value) { this.value = value; }
    public T get() { return value; }
}

Box<String> stringBox = new Box<>();
stringBox.set("Hello");
System.out.println(stringBox.get()); // "Hello"

Box<Integer> intBox = new Box<>();
intBox.set(50);
System.out.println(intBox.get()); // 50

여기서 T는 타입 파라미터(placeholder)입니다. Box<String>이면 T는 String, Box<Integer>면 T는 Integer입니다.


3. 제네릭 메서드

public static <T> void printArray(T[] array) {
    for (T item : array) System.out.println(item);
}

printArray(new String[]{"Alice", "Bob"});
printArray(new Integer[]{1, 2, 3});

메서드 선언 앞쪽에 <T> 타입 파라미터 명시, 다양한 타입의 배열에 활용 가능.


4. 제네릭 인터페이스

인터페이스도 제네릭 작성 가능:

public interface Pair<K, V> {
    K getKey();
    V getValue();
}

public class OrderedPair<K, V> implements Pair<K, V> {
    private K key;
    private V value;
    public OrderedPair(K key, V value) {
        this.key = key; this.value = value;
    }
    public K getKey() { return key; }
    public V getValue() { return value; }
}

K, V처럼 여러 타입 파라미터 동시 사용 가능.


5. 타입 제한(Bound): extends & super

제네릭 타입을 특정 상위클래스로 제한할 수 있습니다:

class NumberBox<T extends Number> { ... }

T는 반드시 Number의 하위클래스여야 함(Integer, Double 등).

와일드카드도 활용:


6. 컬렉션과 제네릭

자바의 핵심 컬렉션(리스트, 맵 등)은 모두 제네릭으로 설계됨:

List<String> list = new ArrayList<>();
list.add("Apple");
String fruit = list.get(0); // 형변환 불필요

Map<String, List<String>> map = new HashMap<>();

원소 타입을 명확히 선언해 해당 타입만 추가 가능.


7. Raw Type(비제네릭 타입) vs 제네릭 타입


8. 타입 추론 & 다이아몬드 연산자

자바 7부터, 인스턴스 생성 시 다이아몬드(<>) 연산자 사용 가능:

List<String> list = new ArrayList<>();

변수 선언에 따라 타입을 자동 추론.


9. 베스트 프랙티스


10. 실무 예시


요약:
제네릭을 활용하면 타입에 대한 추상화와 유연성, 타입 안전성, 재사용성을 동시에 가질 수 있습니다. 컬렉션, 유틸리티 메서드, API 등 반복적 로직을 다양한 타입에 적용하면서도 안전하게 개발할 수 있습니다.


제네릭 메서드와 인터페이스에 대해 조금 더 알아보겠습니다.


1. 제네릭 메서드

기본 문법

public static <T> void myMethod(T param) { ... }

예시

public static <T> void printArray(T[] array) {
    for (T item : array) {
        System.out.println(item);
    }
}

String[] names = {"Alice", "Bob"};
Integer[] nums = {1, 2, 3};
printArray(names);   // Alice, Bob
printArray(nums);    // 1, 2, 3

반환 타입이 제네릭인 예시

public static <T> T getFirst(T[] array) {
    return array.length > 0 ? array : null;
}

바운드 사용 예시

public static <T extends Number> void showNum(T num) {
    System.out.println(num.doubleValue());
}

다중 타입 파라미터 예시

public static <K,V> void printPair(K key, V value) {
    System.out.println(key + " : " + value);
}

2. 제네릭 인터페이스

기본 문법

public interface Container<T> {
    void set(T value);
    T get();
}

예시

public interface Pair<K, V> {
    K getKey();
    V getValue();
}

public class OrderedPair<K, V> implements Pair<K, V> {
    private final K key;
    private final V value;
    public OrderedPair(K key, V value) {
        this.key = key;
        this.value = value;
    }
    public K getKey() { return key; }
    public V getValue() { return value; }
}

Pair<String, Integer> pair = new OrderedPair<>("나이", 30);
System.out.println(pair.getKey());   // 나이
System.out.println(pair.getValue()); // 30

바운드 타입 인터페이스 예시

public interface NumberContainer<T extends Number> {
    void setNumber(T num);
    T getNumber();
}

3. 설계/활용 포인트


요약
제네릭 메서드는 반환 타입 앞에 <T>를 명시하여 다양한 타입에 안전하게 대응하며, 제네릭 인터페이스는 구현 시 타입 명시를 통해 재사용과 안정성을 높입니다. 컬렉션 및 비교, 유틸 API 등 자바에서 필수적인 설계 패턴입니다.


Java의 제네릭 용법에서 경계(Boundaries, extends & super) 에 대해 조금 더 알아보겠습니다.


1. 상한 경계(Upper Bound): extends

기본 문법

class Box<T extends Number> { ... }

예시

public class Bound<T extends A> {
    private T objRef;
    public Bound(T obj) { this.objRef = obj; }
    public void doRunTest() { objRef.displayClass(); }
}

class A { public void displayClass() { System.out.println("A"); } }
class B extends A { public void displayClass() { System.out.println("B"); } }
class C extends A { public void displayClass() { System.out.println("C"); } }

public class BoundedClass {
    public static void main(String[] args) {
        Bound<C> bec = new Bound<>(new C());  // OK
        bec.doRunTest();  // "C"
        Bound<B> beb = new Bound<>(new B());  // OK
        beb.doRunTest();  // "B"
        Bound<A> bea = new Bound<>(new A());  // OK
        bea.doRunTest();  // "A"
        // Bound<String> bes = new Bound<>(new String()); // 컴파일 오류 (A를 상속하지 않음)
    }
}

A 혹은 그 하위 클래스만 Bound<?>에 쓸 수 있고, 다른 타입은 컴파일 오류.

인터페이스/다중 경계

<T extends SuperClass & Interface1 & Interface2>

2. 하한 경계(Lower Bound): super

기본 문법

List<? super Integer> list;

예시

public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 5; i++)
        list.add(i);
}
public static void main(String[] args) {
    List<Number> numberList = new ArrayList<>();
    addNumbers(numberList); // 가능
    System.out.println(numberList); // [1, 2, 3, 4, 5]
    // List<Double> doubleList = new ArrayList<>();
    // addNumbers(doubleList); // 컴파일 오류
}

3. 다중 경계(Multiple Bounds)

<T extends ClassName & Interface1 & Interface2>

4. 와일드카드와 경계의 쓰임

문법 의미 활용
<T extends Number> T는 Number 또는 하위클래스만 타입 안정성 보장, 읽기 작업
<? extends Number> Number 또는 하위타입 만 가능 읽기 작업 증가, 불변에 적합
<? super Integer> Integer 혹은 그 상위타입만 가능 쓰기 작업 증가, 소비자 패턴

결론

상한 경계(extends)는 지정된 타입의 하위 타입만 허용하고, 하한 경계(super)는 지정된 타입의 상위 타입만 허용하여 타입 안정성과 유연성을 동시에 제공합니다. 다중 경계는 여러 인터페이스/한 클래스까지 묶어 더욱 강력한 제네릭 제어가 가능합니다.

references